#!/usr/bin/env python

# Copyright 2012 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

""" Street Identity Client """

import logging
import urllib
from openidconnect.client import OpenIDConnectCredentials, OpenIDConnectFlow
from streetidentity_rp import utils
from streetidentity_rp.access_token import AccessToken
from streetidentity_rp.attribute import Attribute, StreetAddress, Age, Gender
from streetidentity_rp.attributeinfo import AttributeListInfo
from streetidentity_rp.attributeinfo import AuthorizedAttributeInfo
from streetidentity_rp.error import FlowStreetIdentityError, DataError
from oauth2client.anyjson import simplejson

import httplib2
from oauth2client.client import   Flow

__author__ = "Maciej Machulak"
__maintainer__ = "Maciej Machulak"
__email__ = "mmachulak@google.com"

__copyright__ = "Copyright 2012 Google Inc. All Rights Reserved."
__license__ = "Apache License 2.0"
__version__ = "0.2"
__status__ = "Prototype"

STREETIDENTITY_ATTRIBUTE_ID = "si_id"
STREETIDENTITY_TYPE_PARAM = "si_type"
STREETIDENTITY_ACCESS_TOKEN_PARAM = "si_access_token"


class AttributeType(object):
    """Supported attribute types."""

    STREET_ADDRESS = 'street_address'
    AGE = 'age'
    GENDER = 'gender'

    __known_attributes = [STREET_ADDRESS, AGE, GENDER]

    __attribute_class = {
        STREET_ADDRESS: StreetAddress,
        AGE: Age,
        GENDER: Gender
    }

    @staticmethod
    def contains(name):
        index = AttributeType.__known_attributes.count(name)
        if index > 0:
            return AttributeType.__known_attributes[index]
        return None

    @staticmethod
    def get_class(name):
        return AttributeType.__attribute_class.get(name, None)


class AttributeCredentials(object):
    """Credentials for accessing a specific attribute at AP."""

    def __init__(self, oauth_credentials, authorized_attribute_info):
        self.oauth_credentials = oauth_credentials
        self.authorized_attribute_info = authorized_attribute_info


class StreetIdentityCredentials(OpenIDConnectCredentials):
    """Credentials authorized for accessing the Street Identity API endpoints.
    """

    def __init__(self, openidconnect_credentials, attribute_credentials_list):
        OpenIDConnectCredentials.__init__(self,
            openidconnect_credentials,
            openidconnect_credentials.tokeninfo)

        self.attribute_credentials_list = attribute_credentials_list


class ExtendedStreetIdentityCredentials(StreetIdentityCredentials):
    """Credentials with obtained list of attribute info elements
    for a specific user."""

    def __init__(self, streetidentity_credentials, attribute_list):
        StreetIdentityCredentials.__init__(self,
            streetidentity_credentials,
            streetidentity_credentials.attribute_credentials_list)
        self.attribute_list = attribute_list


class StreetIdentityAttributeProviderFlow(Flow):
    """Flow for the Relying Party to be executed against the Attribute Provider.
    """
    def __init__(self,
                 attributes_uri):
        super(StreetIdentityAttributeProviderFlow, self).__init__()
        self.attributes_uri = attributes_uri

    def step7_get_attribute(self,
                            credentials,
                            attribute_id,
                            attribute_access_token,
                            http=None):
        if http is None:
            http = httplib2.Http()

        # authorize the API call
        http = credentials.authorize(http)
        api_uri = self.attributes_uri

        # add identifier to the URI
        api_uri = api_uri + attribute_id

        # add access token for the attribute
        api_uri = utils.add_param(api_uri,
            STREETIDENTITY_ACCESS_TOKEN_PARAM,
            attribute_access_token)

        resp, content = http.request(api_uri,
            headers={'accept-encoding': ''})

        if resp.status == 200:
            data = simplejson.loads(content)
            temp_attribute = Attribute(data)
            clazz = AttributeType.get_class(temp_attribute.type)
            attribute = clazz(data)
            logging.debug('Successfully retrieved attribute: %s', attribute)
            return attribute
        else:
            logging.error('Failed to retrieve attribute: %s', content)
            error_msg = 'Invalid response %s.' % resp['status']
            try:
                data = simplejson.loads(content)
                if 'error' in data:
                    error_msg = data['error']
            except Exception:
                pass

            raise FlowStreetIdentityError(error_msg)

    def step7_get_attribute_from_credentials(self,
                            credentials,
                            http=None):

        return self.step7_get_attribute(
            credentials.oauth_credentials,
            credentials.authorized_attribute_info.attribute_info.id_,
            credentials.authorized_attribute_info.access_token.access_token,
            http
        )


class StreetIdentityFlow(OpenIDConnectFlow):
    """Street Identity Flow for the Relying Party."""

    def __init__(self,
                 scope='https://www.googleapis.com/auth/userinfo.profile '
                       'https://www.googleapis.com/auth/userinfo.email '
                       'https://www.googleapis.com/auth/streetidentity.read',
                 attribute_address_uri =
                 'https://www.googleapis.com/oauth2/v1/streetidentity/attribute_address',
                 attribute_age_uri =
                 'https://www.googleapis.com/oauth2/v1/streetidentity/attribute_age',
                 attribute_gender_uri =
                 'https://www.googleapis.com/oauth2/v1/streetidentity/attribute_gender',
                 attribute_token_uri =
                 'https://www.googleapis.com/oauth2/v1/attribute_token',
                 custom_attributes = None,
                 **kwargs):
        """Constructor for StreetIdentityFlow.

        Args:
          attribute_address_uri: string, URI for Street Address Attribute Info
          API endpoint. For convenience defaults to Google's endpoint but any
          Street Identity compliant IDP can be used.
          attribute_age_uri: string, URI for Verified Age Attribute Info
          API endpoint. For convenience defaults to Google's endpoint but any
          Street Identity compliant IDP can be used.
          attribute_gender_uri: string, URI for Verified Gender Attribute Info
          API endpoint. For convenience defaults to Google's endpoint but any
          Street Identity compliant IDP can be used.
          - scope: string or list of strings, scope(s) of the credentials being
          requested. For convenience defaults to Google's OpenID Connect-related
          scopes
          **kwargs: dict, The keyword arguments require the following parameters
                          - client_id: string, client identifier.
                          - client_secret: string client secret.
                          - user_agent: string, HTTP User-Agent to provide for
                          this application.
                          - auth_uri: string, URI for authorization endpoint.
                          For convenience defaults to Google's endpoints but
                          any OAuth 2.0 provider can be used.
                          - token_uri: string, URI for token endpoint. For
                          convenience defaults to Google's endpoints but
                          any OAuth 2.0 provider can be used
                          - any other optional parameters for OAuth 2.0 / OpenID
                          Connect
        """

        super(StreetIdentityFlow, self).__init__(scope=scope, **kwargs)

        self.attribute_token_uri = attribute_token_uri

        self.__attribute_uri_mapping = {
            AttributeType.STREET_ADDRESS: attribute_address_uri,
            AttributeType.AGE: attribute_age_uri,
            AttributeType.GENDER: attribute_gender_uri
        }

        if custom_attributes and isinstance(custom_attributes, dict):
            self.__attribute_uri_mapping.update(custom_attributes)

    def step5_get_attribute_information(self, credentials,
                                        attribute_type_list=None,
                                        http=None):

        # check the list of attribute types
        if not attribute_type_list:
            attribute_type_list = []
        if not len(attribute_type_list):
            raise FlowStreetIdentityError('no attribute type specified.')

        attribute_info_list = []

        for attribute_type in attribute_type_list:

            attribute_uri = self.__attribute_uri_mapping.get(
                attribute_type, None)
            if not attribute_uri:
                raise FlowStreetIdentityError(
                    'invalid attribute type specified')

            temp_list = self.__step5_get_attribute_information(credentials,
                attribute_uri, http)
            attribute_info_list.extend(temp_list.attribute_list)

        return attribute_info_list

    def __step5_get_attribute_information(self,
                                        credentials,
                                        attribute_uri,
                                        http=None):

        if http is None:
            http = httplib2.Http()

        http = credentials.authorize(http)
        api_uri = attribute_uri

        logging.debug('making request to %s', api_uri)
        resp, content = http.request(api_uri, method="GET",
            headers={'accept-encoding': ''}
        )

        logging.debug('response status: %s', resp.status)
        if resp.status == 200:
            data = simplejson.loads(content)
            logging.debug(data)
            try:
                attribute_list_info = AttributeListInfo(data)
            except DataError:
                raise FlowStreetIdentityError
            logging.debug('Successfully retrieved attribute list: %s',
                attribute_list_info)
            return attribute_list_info
        else:
            logging.error('Failed to retrieve attribute list: %s', content)
            error_msg = 'Invalid response %s.' % resp['status']
            try:
                data = simplejson.loads(content)
                logging.debug(data)
                if 'error' in data:
                    error_msg = data['error']
            except Exception:
                pass

            raise FlowStreetIdentityError(error_msg)

    def step6_get_token(self, credentials, attribute_info, http=None):
        if http is None:
            http = httplib2.Http()

        http = credentials.authorize(http)
        resp, content = http.request(self.attribute_token_uri,
            method="POST",
            body=urllib.urlencode({'attribute_id': attribute_info.id_}),
            headers={'content-type': 'application/x-www-form-urlencoded',
                     'accept-encoding': ''}
        )

        logging.debug('response status: %s', resp.status)
        if resp.status == 200:
            data = simplejson.loads(content)
            logging.debug(data)
            access_token = AccessToken(data)
            logging.debug(
                'Successfully retrieved access token for attribute: %s',
                access_token)
            return AuthorizedAttributeInfo(attribute_info, access_token)
        else:
            logging.error('Failed to retrieve access token: %s', content)
            error_msg = 'Invalid response %s.' % resp['status']
            try:
                data = simplejson.loads(content)
                logging.debug(data)
                if 'error' in data:
                    error_msg = data['error']
            except Exception:
                pass

            raise FlowStreetIdentityError(error_msg)
